# Fishtank
require 'uuidtools'
require 'sinatra/base'
require 'sinatra/contrib/all'
require 'rest_client'
require 'socket'
require 'logger'
require 'sys/uname'
require 'base64'
require 'json'
require 'dm-core'
require 'dm-validations'
require 'dm-timestamps'
require 'dm-migrations'
require 'dm-serializer'
require 'open-uri'
require 'timeout'
require 'net/ping/tcp'
require 'fishtank/fish'
require 'fishtank/fishconfig'
require 'fishtank/fishbrain'
require 'fishtank/finshake'

include Sys

$L = Logger.new(STDOUT)
$L.level = $LOG_LEVEL

class Fishtank < Sinatra::Base
  enable :logging, :dump_errors, :raise_errors, :show_exceptions
  register Sinatra::Contrib

  ##
  ## PUT
  ##
  put '/finshake' do
    data = JSON.parse(request.body.string)
    $L.debug "  Request:  " + request.body.string
    if data.nil? or !data.has_key?('uuid') or !data.has_key?('uri') then
      $L.warn "Invalid finshake request from #{data['uri']}!"
      status 400
    else
      $L.info "Successful finshake request from #{data['uri']}, added as friend"
      create_fish_friend(data['uuid'], data['uri'], data['os'])
      status 200

      # now answer with own identiy information
      body(get_myself().to_json)
      $L.debug "  Responds: " + get_myself.to_json
    end
  end

  put '/brain' do
    referrer = request.env['HTTP_REFERRER']
    $L.info "Brain being pushed in by " + referrer
    data = JSON.parse(request.body.string)
    dec = Base64.decode64(data[0]['brain'])
    File.open("fishtank/fishbrain.rb", "wb") { |f| f.write(dec) }
    load $FISH_BRAIN
    $L.info "  brain reloaded successfully"
  end

  ##
  ## GET
  ##
  get '/finshake' do
    Finshake.init_finshake(self, request.env['HTTP_REFERRER'])
  end

  get '/brain/reload' do
    $L.info "Relading fishbrain"
    load $FISH_TANK
  end

  get '/brain' do
    status 200
    content_type 'application/json'
    get_brain_64().to_json
  end

  get '/brainz' do
    $L.info "Swarm Brain reloading triggered"
    referrer = request.env['HTTP_REFERRER']
    $L.info "  reloading local brain"
    load $FISH_BRAIN
    local_brain = get_brain_64()

    get_friends().each do |f|
      url = "#{f['uri']}/brain"
      if f['uri'] == referrer then
        $L.info "  not pushing to #{url}, is referrer"
      else
        $L.info "  pushing local brain to #{url} with REFERRER #{get_myself['uri']}"
        RestClient.put(url, local_brain.to_json, :referrer => get_myself['uri'])
      end
    end

    status 200
  end

  get '/' do
    @items, @headers = [], []
    content = JSON.parse(RestClient.get("#{get_myself()['uri']}/swarm/whoami"))

    @headers = ["uri", "os", "fish_md5", "fishtank_md5", "fishbrain_md5"]

    content.each do |c|
      @items << [c['uri'], c['os'], c['fish_md5'], c['fishtank_md5'], c['fishbrain_md5']]
    end
    haml :status
  end


  get '/alive' do
    status 200
    content_type 'application/json'
    [:alive => 'true'].to_json()
  end

  get '/:method' do
    content = parse_brain_output()
    status 200
    content_type 'application/json'
    body(content.to_json())
  end

  get '/swarm/:method' do
    referrer = request.env['HTTP_REFERRER']
    content_local, content_friend = [], []
    $L.info "Swarm call to /swarm/#{params[:method]} from #{referrer}"

    if params['fishbrain_md5'] and params['fishbrain_md5'] != get_myself()['fishbrain_md5'] then
      $L.warn "  brain not sane (md5 hash differs)!"
      status 200
      content_type 'application/json'
      "{}"
    elsif $working
      $L.debug "  already busy, doing nothin"
      status 200
      "{}"
    else
      $working = true

      # get local contant
      content_local << parse_brain_output()

      # get content from friends
      get_friends().each do |f|
        url = "#{f['uri']}/swarm/#{params[:method]}"
        if f['uri'] == referrer then
          $L.info "  not asking #{url}, is referrer"
        else
          $L.info "  asking #{url} with REFERRER #{get_myself['uri']}"
          content_friend << JSON.parse(RestClient.get(url, :referrer => get_myself['uri'], :params => {:fishbrain_md5 => "#{get_myself()['fishbrain_md5']}"}))
        end
      end

      # strip away the addional array
      if !content_friend.nil? and content_friend.length > 0
        content_friend[0].each do |c|
          content_local << c
        end
      end

      status 200
      content_type 'application/json'
      body(content_local.to_json)

      $working = false
    end
  end

  get '/friends' do
    if get_friends.count < 1 then
      $L.info "No friend(s) yet :/ Call /finshake to search for then ..."
      status 404
    else
      status 200
      content_type 'application/json'
      get_friends.to_json
    end
  end

  ##
  ## Helpers
  ##
  helpers do

    def parse_brain_output()
      begin
        $L.info "Calling fishbrain with #{params[:method]}"
        if Fishbrain.respond_to?(params[:method])
          content_local = JSON.parse(Fishbrain.send(params[:method], self))
        else
          $L.info "  Fisbrain does not support that method!"
        end
      rescue Exception=>e
        raise e, "Brain output not valid JSON!"
      end
      content_local
    end

    def get_brain_64()
      file = File.open("fishtank/fishbrain.rb", "rb")
      brain64 = Base64.encode64(file.read)
      brain64.gsub!(/\n/, "")
      ["brain" => brain64]
    end

    # Returns datamapper friends object
    def get_friends()
      FishFriend.all()
    end

    # Returns datamapper myself object
    def get_myself()
      Fish.first()
    end

    def img(name)
      "<img src='#{name}' alt='#{name}' />"
    end

    def a(link)
      "<a href='#{link}'>'#{link}'</a>"
    end

    # OS Checker
    def check_os()
      begin
        if Uname.uname.sysname == "Linux"
          "Linux"
        elsif Uname.uname.sysname == "Solaris"
          "Solaris"
        end
      rescue Exception=>e
        "Win"
      end
    end

    # create a new fish friend entry
    # @param uuid [Object]
    # @param uri [Object]
    # @param os [Object]
    def create_fish_friend(uuid, uri, os)
      fish_friend = FishFriend.new(
          :uuid => uuid,
          :uri => uri,
          :os => os,
          :created_at => Time.now,
          :updated_at => Time.now
      )
      fish_friend.fish = get_myself()
      fish_friend.save
    end


    # Temperature Module
    def query_temp()
      content = ""
      referrer = request.env['HTTP_REFERRER']
      $L.info "Temp Module called from " + referrer.inspect()

      # first call local
      $L.info "  Calling local"
      if check_os == "Linux"
        $L.debug "    OS: Linux"
        content << JSON.parse(Hdd.getTempLinux)
      elsif check_os == "Win"
        $L.debug "    OS: Win"
        $L.warn "    Win not supported!"
      end

      # now call friends
      if Fish.count > 0
        $L.info "  Calling friends"
        responds = ""
        Fish.all.each do |f|
          # but only if friend is not the one this method was called from
          if f.uri != referrer then
            uri = f.uri + "/s/hdd/temp"
            $L.debug "    at URI " + uri
            content << JSON.parse(RestClient.get("#{uri}", :referrer => get_myself['uri']))
          else
            $L.debug "    Not calling " + f.uri + " as the request came from HTTP_REFERRER " + referrer
          end
        end
      end

      content
    end
  end

  # Get local IP. This opens a socket to an external ip! Meaning: it will return your external IP (VPN anyone?)
  # http://stackoverflow.com/questions/42566/getting-the-hostname-or-ip-in-ruby-on-rails
  def self.local_ip()
    #  orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true # turn off reverse DNS resolution temporarily
    #  UDPSocket.open do |s|
    #    s.connect '64.233.187.99', 1
    #    s.addr.last
    #  end
    #ensure
    #  Socket.do_not_reverse_lookup = orig

    Socket.ip_address_list.detect { |intf| intf.ipv4_private? }.ip_address

  end

  # Search for full qualified path info in *$:* for passed file_name
  # @param file_name [String]
  # @returns first found path or nil if none could be found
  def self.which(file_name)
    file_path = nil
    $:.each do |p|
      joined_path = File.join(p, file_name)
      if File.exist? joined_path
        $L.debug "  found #{file_name} at #{joined_path}"
        file_path = joined_path
        break
      end
    end
    file_path
  end

  ##
  ## Configure, runs at application start up
  ##
  configure do
    # generate UUID
    unless $UUID = UUIDTools::UUID.random_create
      raise "UUID could not be generated!"
    end
    $L.info "INIT $UUID => " + $UUID

    # local ip
    unless $LOCAL_IP = local_ip()
      raise "Local IP could not be retrieved!"
    end
    $L.info "INIT $LOCAL_IP => " + $LOCAL_IP

    # init some path information
    $L.debug "INIT Path look up"
    $FISH_BRAIN = which("fishtank/fishbrain.rb")
    $FISH_TANK = which("fishtank.rb")
    $FISH = which("fishtank/fish.rb")
    unless $FISH_BRAIN and $FISH_TANK and $FISH
      raise "Could not retrieve path information for $FISH_BRAIN, $FISH_TANK or $FISH!"
    end

  end

  def get_uri(request)
    "#{request.scheme}://#{$LOCAL_IP}:#{request.port}"
  end

  ##
  ## Before
  ##
  before do
    # before every call we check if we have already a 'self' entry in the database
    if get_myself().nil?
      $L.info "INIT creating myself with URI #{get_uri(request)}"
      fish = Fish.create(
          'uuid' => $UUID,
          'uri' => get_uri(request),
          'os' => check_os()
      )
    end

    # update md5's
    $L.debug "Updating MD5 of " + $FISH_BRAIN + ", " + $FISH_TANK + ", " + $FISH
    get_myself().update(
        :fishbrain_md5 => Digest::MD5.hexdigest(File.open($FISH_BRAIN, "rb") { |f| f.read }),
        :fishtank_md5 => Digest::MD5.hexdigest(File.open($FISH_TANK, "rb") { |f| f.read }),
        :fish_md5 => Digest::MD5.hexdigest(File.open($FISH, "rb") { |f| f.read })
    )

  end

#  # this url is used for finshaking, therefore we exclude it (would cause a loop)
#  after /^(?!\/(alive))/ do
#    # if we are in swarm mode, we'll try to find at least one friend
#
#  end

  run!

end


